<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>多功能文件管理器 - LayUI</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/layui@2.8.18/dist/css/layui.css" />
  <style>
    html, body { height: 100%; }
    .layout { display: flex; height: calc(100vh - 60px); }
    .sidebar { width: 260px; border-right: 1px solid #eee; overflow: auto; }
    .main { flex: 1; display: flex; flex-direction: column; }
    .right { width: 420px; border-left: 1px solid #eee; overflow: auto; }
    .toolbar { padding: 8px; border-bottom: 1px solid #eee; display: flex; gap: 8px; align-items: center; }
    .breadcrumb { padding: 8px 12px; border-bottom: 1px solid #eee; background: #fafafa; }
    .file-table { flex: 1; overflow: auto; }
    .info-panel { padding: 12px; }
    .preview-panel { padding: 12px; }
    .preview-panel img, .preview-panel video, .preview-panel iframe, .preview-panel pre, .preview-panel textarea {
      width: 100%; max-height: 60vh;
    }
    .favorites-panel { padding: 12px; }
    .favorites-list .item { display:flex; justify-content:space-between; align-items:center; padding:6px 0; border-bottom:1px dashed #eee; }
    .inline-input { width: 260px; }
    .hidden { display: none; }
    .name-cell { cursor: pointer; }
    .name-cell .layui-icon { margin-right: 6px; }
    .status { padding: 6px 12px; border-top: 1px solid #eee; background: #fafafa; }
  </style>
</head>
<body class="layui-container">

  <div class="toolbar">
    <button class="layui-btn layui-btn-sm" id="btnUp">
      <i class="layui-icon layui-icon-up"></i> 上一级
    </button>
    <button class="layui-btn layui-btn-sm" id="btnNewFolder">
      <i class="layui-icon layui-icon-add-circle"></i> 新建文件夹
    </button>
    <button class="layui-btn layui-btn-sm" id="btnMove">
      <i class="layui-icon layui-icon-transfer"></i> 移动
    </button>
    <button class="layui-btn layui-btn-sm" id="btnCopy">
      <i class="layui-icon layui-icon-file"></i> 拷贝
    </button>
    <button class="layui-btn layui-btn-sm layui-btn-danger" id="btnDelete">
      <i class="layui-icon layui-icon-delete"></i> 删除
    </button>
    <button class="layui-btn layui-btn-sm" id="btnRename">
      <i class="layui-icon layui-icon-edit"></i> 重命名
    </button>
    <button class="layui-btn layui-btn-sm" id="btnRefresh">
      <i class="layui-icon layui-icon-refresh-3"></i> 刷新
    </button>
    <button class="layui-btn layui-btn-sm layui-btn-warm" id="btnFavorites">
      <i class="layui-icon layui-icon-star"></i> 常用文件夹
    </button>
    <span style="margin-left:auto;">
      <input id="searchInput" class="layui-input inline-input" placeholder="搜索当前目录（名称包含）" />
    </span>
  </div>

  <div class="breadcrumb">
    当前位置：<span id="breadcrumb"></span>
    <button class="layui-btn layui-btn-xs" id="btnAddFavorite" style="margin-left:8px;"><i class="layui-icon layui-icon-rate-solid"></i> 收藏当前</button>
  </div>

  <div class="layout">
    <div class="sidebar">
      <div id="dirTree"></div>
    </div>

    <div class="main">
      <div class="file-table">
        <table id="fileTable" lay-filter="fileTable"></table>
      </div>
      <div class="status" id="statusBar">就绪</div>
    </div>

    <div class="right">
      <div class="layui-tab" lay-filter="rightTab">
        <ul class="layui-tab-title">
          <li class="layui-this">信息</li>
          <li>预览/编辑</li>
        </ul>
        <div class="layui-tab-content">
          <div class="layui-tab-item layui-show">
            <div class="info-panel" id="infoPanel">
              <div>请选择文件或文件夹查看信息</div>
            </div>
          </div>
          <div class="layui-tab-item">
            <div class="preview-panel" id="previewPanel">
              <div>请选择文件进行预览或编辑</div>
              <div id="textViewer" class="hidden">
                <pre id="textContent" style="white-space: pre-wrap;"></pre>
              </div>
              <div id="textEditor" class="hidden">
                <textarea id="textArea" rows="24" class="layui-textarea"></textarea>
                <div style="margin-top:8px;">
                  <button class="layui-btn layui-btn-sm" id="btnSaveText"><i class="layui-icon layui-icon-ok"></i> 保存</button>
                  <button class="layui-btn layui-btn-sm layui-btn-primary" id="btnCancelEdit"><i class="layui-icon layui-icon-close"></i> 取消</button>
                </div>
              </div>
              <div id="imgViewer" class="hidden"><img id="imgPreview" /></div>
              <div id="videoViewer" class="hidden"><video id="videoPreview" controls></video></div>
              <div id="pdfViewer" class="hidden"><iframe id="pdfFrame" style="height:60vh;"></iframe></div>
              <div id="otherViewer" class="hidden"><div>该文件类型不支持在线预览。</div></div>
              <div style="margin-top:8px;">
                <button class="layui-btn layui-btn-xs" id="btnEditText" class="hidden"><i class="layui-icon layui-icon-edit"></i> 编辑文本</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

  </div>

  <!-- 常用文件夹抽屉内容（隐藏，layer 打开） -->
  <div id="favoritesDrawer" class="favorites-panel hidden">
    <div style="display:flex; gap:8px; margin-bottom:8px;">
      <input id="favNameInput" class="layui-input" placeholder="名称（可选）" />
      <input id="favPathInput" class="layui-input" placeholder="路径（留空默认当前路径）" />
      <button class="layui-btn layui-btn-sm" id="btnAddFavInDrawer">添加</button>
    </div>
    <div class="favorites-list" id="favoritesList"></div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/layui@2.8.18/dist/layui.js"></script>
  <script>
    layui.config({}).use(['element', 'tree', 'table', 'layer', 'dropdown', 'util', 'form'], function(){
      const element = layui.element;
      const tree = layui.tree;
      const table = layui.table;
      const layer = layui.layer;
      const dropdown = layui.dropdown;
      const util = layui.util;

      // ---------------- 配置 ----------------
      const API_BASE = '/api';
      const TEXT_EXT = ['.txt', '.md', '.json', '.js', '.ts', '.py', '.java', '.c', '.cpp', '.h', '.css', '.scss', '.less', '.html', '.xml', '.yml', '.yaml', '.csv', '.log', '.ini', '.conf', '.sh', '.bat', '.ps1'];
      const IMG_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'];
      const VIDEO_EXT = ['.mp4', '.webm', '.ogg'];
      const PDF_EXT = ['.pdf'];

      let currentPath = '/';
      let currentSelection = null; // 当前选中的文件或目录（用于信息与预览）
      let favoritesDrawerIndex = null;
      let isEditing = false;

      // ---------------- 工具函数 ----------------
      function $(sel){ return document.querySelector(sel); }
      function $all(sel){ return Array.from(document.querySelectorAll(sel)); }
      function esc(s){ return (s||'').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
      function joinPath(a,b){
        if(!a || a === '/') return '/' + (b||'').replace(/^\/+/, '');
        return (a.replace(/\/+$/,'') + '/' + (b||'').replace(/^\/+/, ''));
      }
      function dirname(p){
        if(!p || p === '/') return '/';
        const parts = p.split('/').filter(Boolean);
        parts.pop();
        return '/' + parts.join('/');
      }
      function basename(p){
        if(!p || p === '/') return '/';
        const parts = p.split('/').filter(Boolean);
        return parts.pop() || '/';
      }
      function extname(name){
        const i = (name||'').lastIndexOf('.');
        return i>=0 ? name.slice(i).toLowerCase() : '';
      }
      function formatSize(bytes){
        if(bytes == null || bytes < 0) return '';
        const units = ['B','KB','MB','GB','TB'];
        let i=0, n=bytes;
        while(n>=1024 && i<units.length-1){ n/=1024; i++; }
        return (n.toFixed(n<10 && i>0 ? 2 : 0)) + ' ' + units[i];
      }
      async function fetchJSON(url, opts={}){
        const res = await fetch(url, { headers: {'Content-Type':'application/json'}, credentials: 'same-origin', ...opts });
        if(!res.ok){ throw new Error('HTTP ' + res.status + ' ' + await res.text()); }
        return res.json();
      }
      function setStatus(msg){ $('#statusBar').textContent = msg; }

      // ---------------- 目录树（懒加载） ----------------
      const dirTree = tree.render({
        elem: '#dirTree',
        id: 'dirTree',
        showLine: true,
        onlyIconControl: true,
        accordion: true,
        lazy: true,
        data: [{ title: '根目录', id: '/', path: '/', spread: true, children: [] }],
        load: async function(node, resolve){
          try {
            const parentPath = node.data ? node.data.path : '/';
            const res = await fetchJSON(`${API_BASE}/fs/list?path=${encodeURIComponent(parentPath)}&onlyDir=1`);
            const nodes = res.data.map(d => ({
              title: d.name,
              id: d.path,
              path: d.path,
              children: d.hasChildren ? [] : null
            }));
            resolve(nodes);
          } catch(e){ layer.msg('加载目录失败：' + e.message, {icon:2}); resolve([]); }
        },
        click: function(obj){
          openPath(obj.data.path);
        }
      });

      // ---------------- 文件表格 ----------------
      const tableInst = table.render({
        elem: '#fileTable',
        id: 'fileTable',
        height: 'full-220',
        page: false,
        limit: 1000,
        size: 'sm',
        cols: [[
          {type:'checkbox', width: 40},
          {field:'name', title:'名称', minWidth: 240, templet: d => {
            const icon = d.isDir ? 'layui-icon-folder' : 'layui-icon-file';
            return `<span class="name-cell" data-path="${esc(d.path)}" data-isdir="${d.isDir}"><i class="layui-icon ${icon}"></i>${esc(d.name)}</span>`;
          }},
          {field:'type', title:'类型', width: 100, templet: d => d.isDir ? '文件夹' : (extname(d.name) || '文件')},
          {field:'size', title:'大小', width: 110, align:'right', templet: d => d.isDir ? '' : formatSize(d.size)},
          {field:'mtime', title:'修改时间', width: 180, templet: d => util.toDateString(d.mtime, 'yyyy-MM-dd HH:mm:ss')}
        ]],
        data: []
      });

      // 行事件：点击名称
      document.body.addEventListener('click', (e)=>{
        const cell = e.target.closest('.name-cell');
        if(cell){
          const path = cell.getAttribute('data-path');
          const isDir = cell.getAttribute('data-isdir') === 'true';
          if(isDir) openPath(path);
          else openFile(path);
        }
      });

      // 表格工具
      async function loadTable(path, keyword=''){
        setStatus('加载中...');
        try {
          const res = await fetchJSON(`${API_BASE}/fs/list?path=${encodeURIComponent(path)}`);
          let rows = res.data || [];
          if(keyword){
            const k = keyword.toLowerCase();
            rows = rows.filter(r => (r.name || '').toLowerCase().includes(k));
          }
          table.reload('fileTable', { data: rows });
          setStatus(`已加载：${path}（${rows.length} 项）`);
        } catch(e){
          layer.msg('加载失败：' + e.message, {icon:2});
          setStatus('失败');
        }
      }

      function renderBreadcrumb(path){
        const parts = path.split('/').filter(Boolean);
        let acc = '/';
        const spans = ['<a href="#" data-path="/">/</a>'];
        for(const p of parts){
          acc = joinPath(acc, p);
          spans.push(`<span> / </span><a href="#" data-path="${esc(acc)}">${esc(p)}</a>`);
        }
        $('#breadcrumb').innerHTML = spans.join('');
      }
      $('#breadcrumb').addEventListener('click', (e)=>{
        const a = e.target.closest('a[data-path]');
        if(a){ e.preventDefault(); openPath(a.getAttribute('data-path')); }
      });

      async function openPath(path){
        currentPath = path || '/';
        renderBreadcrumb(currentPath);
        await loadTable(currentPath, $('#searchInput').value.trim());
        currentSelection = null;
        renderInfo(null);
        clearPreview();
      }

      // 顶部按钮
      $('#btnUp').addEventListener('click', ()=> openPath(dirname(currentPath)));
      $('#btnRefresh').addEventListener('click', ()=> openPath(currentPath));
      $('#searchInput').addEventListener('input', util.throttle(function(){
        loadTable(currentPath, this.value.trim());
      }, 300));

      // 新建文件夹
      $('#btnNewFolder').addEventListener('click', ()=>{
        layer.prompt({ title: '新建文件夹名称', formType: 0 }, async function(value, index){
          layer.close(index);
          if(!value.trim()) return;
          try {
            await fetchJSON(`${API_BASE}/fs/mkdir`, { method:'POST', body: JSON.stringify({ parent: currentPath, name: value.trim() }) });
            layer.msg('创建成功', {icon:1});
            openPath(currentPath);
          } catch(e){ layer.msg('创建失败：' + e.message, {icon:2}); }
        });
      });

      // 删除
      $('#btnDelete').addEventListener('click', async ()=>{
        const checked = table.checkStatus('fileTable').data;
        if(checked.length === 0) return layer.msg('请先选择要删除的项');
        layer.confirm(`确认删除选中的 ${checked.length} 项？该操作不可撤销。`, async function(idx){
          layer.close(idx);
          try{
            await fetchJSON(`${API_BASE}/fs/delete`, { method:'POST', body: JSON.stringify({ paths: checked.map(r=>r.path) }) });
            layer.msg('删除成功', {icon:1});
            openPath(currentPath);
          } catch(e){ layer.msg('删除失败：' + e.message, {icon:2}); }
        });
      });

      // 重命名
      $('#btnRename').addEventListener('click', ()=>{
        const checked = table.checkStatus('fileTable').data;
        if(checked.length !== 1) return layer.msg('请选择且仅选择 1 项进行重命名');
        const row = checked[0];
        layer.prompt({ title: `重命名：${row.name}`, value: row.name, formType: 0 }, async function(value, index){
          layer.close(index);
          const newName = value.trim();
          if(!newName || newName === row.name) return;
          try{
            await fetchJSON(`${API_BASE}/fs/rename`, { method:'POST', body: JSON.stringify({ path: row.path, newName }) });
            layer.msg('重命名成功', {icon:1});
            openPath(currentPath);
          }catch(e){ layer.msg('重命名失败：' + e.message, {icon:2}); }
        });
      });

      // 移动 / 拷贝
      async function chooseDestAndDo(op){
        const checked = table.checkStatus('fileTable').data;
        if(checked.length === 0) return layer.msg(`请先选择要${op==='move'?'移动':'拷贝'}的项`);
        const content = document.createElement('div');
        content.style.padding = '8px';
        content.innerHTML = `<div id="moveCopyTree"></div><div style="margin-top:8px;color:#666;">选择目标目录后点击确认</div>`;
        const idx = layer.open({
          type: 1, title: (op==='move'?'移动':'拷贝') + '到...',
          area: ['360px', '480px'], content,
          btn: ['确认', '取消'],
          success: ()=>{
            tree.render({
              elem: content.querySelector('#moveCopyTree'),
              showLine:true, onlyIconControl:true, accordion:true, lazy:true,
              data:[{ title:'根目录', id:'/', path:'/', spread:true, children:[] }],
              load: dirTree.config.load,
              click: (obj)=>{ content.dataset.dest = obj.data.path; }
            });
          },
          yes: async ()=>{
            const dest = content.dataset.dest || '/';
            try{
              await fetchJSON(`${API_BASE}/fs/${op}`, { method:'POST', body: JSON.stringify({ paths: checked.map(r=>r.path), dest }) });
              layer.close(idx);
              layer.msg((op==='move'?'移动':'拷贝')+'成功', {icon:1});
              openPath(currentPath);
            }catch(e){ layer.msg((op==='move'?'移动':'拷贝')+'失败：' + e.message, {icon:2}); }
          }
        });
      }
      $('#btnMove').addEventListener('click', ()=> chooseDestAndDo('move'));
      $('#btnCopy').addEventListener('click', ()=> chooseDestAndDo('copy'));

      // 打开常用文件夹抽屉
      $('#btnFavorites').addEventListener('click', openFavoritesDrawer);
      $('#btnAddFavorite').addEventListener('click', ()=> addFavorite('', currentPath));
      $('#btnAddFavInDrawer').addEventListener('click', ()=>{
        const name = $('#favNameInput').value.trim();
        const path = $('#favPathInput').value.trim() || currentPath;
        addFavorite(name, path);
      });

      async function openFavoritesDrawer(){
        if(favoritesDrawerIndex != null){ return; }
        favoritesDrawerIndex = layer.open({
          type:1, title:'常用文件夹', area:['320px','100%'], offset:'r',
          shade: 0.1, anim: 'slideLeft', closeBtn:1,
          content: $('#favoritesDrawer').outerHTML ? $('#favoritesDrawer') : document.getElementById('favoritesDrawer'),
          end: ()=> { favoritesDrawerIndex = null; }
        });
        await loadFavorites();
      }

      async function loadFavorites(){
        try{
          const res = await fetchJSON(`${API_BASE}/favorites`);
          renderFavorites(res.data || []);
        }catch(e){
          layer.msg('加载常用文件夹失败：' + e.message, {icon:2});
          renderFavorites([]);
        }
      }
      function renderFavorites(list){
        const wrap = document.getElementById('favoritesList');
        wrap.innerHTML = '';
        for(const f of list){
          const div = document.createElement('div');
          div.className = 'item';
          div.innerHTML = `
            <div>
              <i class="layui-icon layui-icon-star-fill" style="color:#FFB800;"></i>
              <a href="#" data-path="${esc(f.path)}">${esc(f.name || basename(f.path) || '/')}</a>
              <div style="color:#999;font-size:12px;">${esc(f.path)}</div>
            </div>
            <div>
              <button class="layui-btn layui-btn-xs" data-act="go" data-path="${esc(f.path)}">打开</button>
              <button class="layui-btn layui-btn-xs layui-btn-danger" data-act="del" data-id="${esc(f.id||'')}" data-path="${esc(f.path)}">移除</button>
            </div>
          `;
          wrap.appendChild(div);
        }
        wrap.addEventListener('click', async (e)=>{
          const btn = e.target.closest('button[data-act]');
          const a = e.target.closest('a[data-path]');
          if(btn){
            const act = btn.dataset.act;
            if(act === 'go'){
              openPath(btn.dataset.path);
            }else if(act === 'del'){
              try{
                await fetchJSON(`${API_BASE}/favorites`, { method:'DELETE', body: JSON.stringify({ id: btn.dataset.id, path: btn.dataset.path }) });
                await loadFavorites();
                layer.msg('已移除', {icon:1});
              }catch(e){ layer.msg('移除失败：' + e.message, {icon:2}); }
            }
          }else if(a){
            e.preventDefault();
            openPath(a.dataset.path);
          }
        }, { once: true });
      }
      async function addFavorite(name, path){
        try{
          await fetchJSON(`${API_BASE}/favorites`, { method:'POST', body: JSON.stringify({ name, path }) });
          layer.msg('已收藏', {icon:1});
          if(favoritesDrawerIndex != null) await loadFavorites();
        }catch(e){ layer.msg('收藏失败：' + e.message, {icon:2}); }
      }

      // 文件信息 + 预览/编辑
      function renderInfo(item){
        const info = $('#infoPanel');
        if(!item){
          info.innerHTML = '<div>请选择文件或文件夹查看信息</div>';
          return;
        }
        info.innerHTML = `
          <div><b>名称：</b>${esc(item.name)}</div>
          <div><b>路径：</b>${esc(item.path)}</div>
          <div><b>类型：</b>${item.isDir ? '文件夹' : (extname(item.name) || '文件')}</div>
          <div><b>大小：</b>${item.isDir ? '-' : formatSize(item.size)}</div>
          <div><b>修改时间：</b>${util.toDateString(item.mtime, 'yyyy-MM-dd HH:mm:ss')}</div>
        `;
      }
      function clearPreview(){
        ['textViewer','textEditor','imgViewer','videoViewer','pdfViewer','otherViewer'].forEach(id=>$('#'+id).classList.add('hidden'));
        $('#btnEditText').classList.add('hidden');
        isEditing = false;
      }
      async function openFile(path){
        try{
          const res = await fetchJSON(`${API_BASE}/fs/info?path=${encodeURIComponent(path)}`);
          currentSelection = res.data;
          renderInfo(currentSelection);
          clearPreview();

          if(currentSelection.isDir){ return; }
          const ext = extname(currentSelection.name);

          if(TEXT_EXT.includes(ext)){
            // 拉取文本
            const contentRes = await fetchJSON(`${API_BASE}/fs/read?path=${encodeURIComponent(path)}`);
            $('#textContent').textContent = contentRes.data.content || '';
            $('#textViewer').classList.remove('hidden');
            $('#btnEditText').classList.remove('hidden');
          } else if(IMG_EXT.includes(ext)){
            $('#imgPreview').src = `${API_BASE}/fs/raw?path=${encodeURIComponent(path)}`;
            $('#imgViewer').classList.remove('hidden');
          } else if(VIDEO_EXT.includes(ext)){
            $('#videoPreview').src = `${API_BASE}/fs/raw?path=${encodeURIComponent(path)}`;
            $('#videoViewer').classList.remove('hidden');
          } else if(PDF_EXT.includes(ext)){
            $('#pdfFrame').src = `${API_BASE}/fs/raw?path=${encodeURIComponent(path)}`;
            $('#pdfViewer').classList.remove('hidden');
          } else {
            $('#otherViewer').classList.remove('hidden');
          }
          element.tabChange('rightTab', '1'); // 切换到预览/编辑页签
        }catch(e){
          layer.msg('打开失败：' + e.message, {icon:2});
        }
      }

      // 文本编辑
      $('#btnEditText').addEventListener('click', async ()=>{
        if(!currentSelection || currentSelection.isDir) return;
        try{
          const contentRes = await fetchJSON(`${API_BASE}/fs/read?path=${encodeURIComponent(currentSelection.path)}`);
          $('#textArea').value = contentRes.data.content || '';
          clearPreview();
          $('#textEditor').classList.remove('hidden');
          isEditing = true;
        }catch(e){ layer.msg('读取失败：' + e.message, {icon:2}); }
      });
      $('#btnCancelEdit').addEventListener('click', ()=>{
        isEditing = false;
        clearPreview();
        $('#textViewer').classList.remove('hidden');
      });
      $('#btnSaveText').addEventListener('click', async ()=>{
        if(!currentSelection) return;
        try{
          await fetchJSON(`${API_BASE}/fs/write`, { method:'POST', body: JSON.stringify({ path: currentSelection.path, content: $('#textArea').value }) });
          layer.msg('已保存', {icon:1});
          isEditing = false;
          openFile(currentSelection.path); // 重新加载
          openPath(currentPath); // 刷新列表大小/时间
        }catch(e){ layer.msg('保存失败：' + e.message, {icon:2}); }
      });

      // 初始化
      openPath('/');
    });
  </script>
</body>
</html>